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

This commit is contained in:
Matt Bruce 2025-07-18 17:05:39 -05:00
parent a243e1e034
commit 4b2d1dcf77
7 changed files with 72 additions and 32 deletions

View File

@ -2,7 +2,7 @@ import React from 'react';
import { InfiniteScrollList, SongItem } from '../../components/common'; import { InfiniteScrollList, SongItem } from '../../components/common';
import { useNewSongs } from '../../hooks'; import { useNewSongs } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
import { selectNewSongs } from '../../redux'; import { selectNewSongsArray } from '../../redux';
import { debugLog } from '../../utils/logger'; import { debugLog } from '../../utils/logger';
import type { Song } from '../../types'; import type { Song } from '../../types';
@ -15,8 +15,8 @@ const NewSongs: React.FC = () => {
handleToggleFavorite, handleToggleFavorite,
} = useNewSongs(); } = useNewSongs();
const newSongs = useAppSelector(selectNewSongs); const newSongsArray = useAppSelector(selectNewSongsArray);
const newSongsCount = Object.keys(newSongs).length; const newSongsCount = newSongsArray.length;
// Debug logging // Debug logging
debugLog('NewSongs component - new songs count:', newSongsCount); debugLog('NewSongs component - new songs count:', newSongsCount);

View File

@ -4,7 +4,7 @@ import { trash, reorderThreeOutline, reorderTwoOutline, playCircle } from 'ionic
import { ActionButton } from '../../components/common'; import { ActionButton } from '../../components/common';
import { useQueue } from '../../hooks'; import { useQueue } from '../../hooks';
import { useAppSelector } from '../../redux'; import { useAppSelector } from '../../redux';
import { selectQueue, selectPlayerState, selectIsAdmin, selectControllerName } from '../../redux'; import { selectQueueLength, selectPlayerStateMemoized, selectIsAdmin, selectControllerName } from '../../redux';
import { PlayerState } from '../../types'; import { PlayerState } from '../../types';
import { queueService } from '../../firebase/services'; import { queueService } from '../../firebase/services';
import { debugLog } from '../../utils/logger'; import { debugLog } from '../../utils/logger';
@ -23,11 +23,10 @@ const Queue: React.FC = () => {
handleRemoveFromQueue, handleRemoveFromQueue,
} = useQueue(); } = useQueue();
const queue = useAppSelector(selectQueue); const queueCount = useAppSelector(selectQueueLength);
const playerState = useAppSelector(selectPlayerState); const playerState = useAppSelector(selectPlayerStateMemoized);
const isAdmin = useAppSelector(selectIsAdmin); const isAdmin = useAppSelector(selectIsAdmin);
const controllerName = useAppSelector(selectControllerName); const controllerName = useAppSelector(selectControllerName);
const queueCount = Object.keys(queue).length;
// Debug logging // Debug logging
debugLog('Queue component - queue count:', queueCount); debugLog('Queue component - queue count:', queueCount);

View File

@ -1,13 +1,13 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useAppSelector } from '../redux'; import { useAppSelector } from '../redux';
import { selectControllerName, selectCurrentSinger } from '../redux'; import { selectControllerName, selectCurrentSinger, selectQueueObject } from '../redux';
import { queueService, favoritesService } from '../firebase/services'; import { queueService, favoritesService } from '../firebase/services';
import type { Song, QueueItem } from '../types'; import type { Song, QueueItem } from '../types';
export const useSongOperations = () => { export const useSongOperations = () => {
const controllerName = useAppSelector(selectControllerName); const controllerName = useAppSelector(selectControllerName);
const currentSinger = useAppSelector(selectCurrentSinger); const currentSinger = useAppSelector(selectCurrentSinger);
const currentQueue = useAppSelector((state) => state.controller.data?.player?.queue || {}); const currentQueue = useAppSelector(selectQueueObject);
const addToQueue = useCallback(async (song: Song) => { const addToQueue = useCallback(async (song: Song) => {
if (!controllerName || !currentSinger) { if (!controllerName || !currentSinger) {

View File

@ -72,8 +72,8 @@ export const selectAuth = (state: { auth: AuthState }) => state.auth.data;
export const selectAuthLoading = (state: { auth: AuthState }) => state.auth.loading; export const selectAuthLoading = (state: { auth: AuthState }) => state.auth.loading;
export const selectAuthError = (state: { auth: AuthState }) => state.auth.error; export const selectAuthError = (state: { auth: AuthState }) => state.auth.error;
export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth.data?.authenticated || false; export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth.data?.authenticated || false;
export const selectCurrentSinger = (state: { auth: AuthState }) => state.auth.data?.singer || ''; export const selectCurrentSinger = (state: { auth: AuthState }) => state.auth.data?.singer ?? '';
export const selectIsAdmin = (state: { auth: AuthState }) => state.auth.data?.isAdmin || false; export const selectIsAdmin = (state: { auth: AuthState }) => Boolean(state.auth.data?.isAdmin);
export const selectControllerName = (state: { auth: AuthState }) => state.auth.data?.controller || ''; export const selectControllerName = (state: { auth: AuthState }) => state.auth.data?.controller ?? '';
export default authSlice.reducer; export default authSlice.reducer;

View File

@ -151,14 +151,17 @@ export const selectControllerLoading = (state: { controller: ControllerState })
export const selectControllerError = (state: { controller: ControllerState }) => state.controller.error; export const selectControllerError = (state: { controller: ControllerState }) => state.controller.error;
export const selectLastUpdated = (state: { controller: ControllerState }) => state.controller.lastUpdated; export const selectLastUpdated = (state: { controller: ControllerState }) => state.controller.lastUpdated;
// Constants for empty objects to prevent new references
const EMPTY_OBJECT = {};
// Selectors for specific data // Selectors for specific data
export const selectSongs = (state: { controller: ControllerState }) => state.controller.data?.songs || {}; export const selectSongs = (state: { controller: ControllerState }) => state.controller.data?.songs ?? EMPTY_OBJECT;
export const selectQueue = (state: { controller: ControllerState }) => state.controller.data?.player?.queue || {}; export const selectQueue = (state: { controller: ControllerState }) => state.controller.data?.player?.queue ?? EMPTY_OBJECT;
export const selectFavorites = (state: { controller: ControllerState }) => state.controller.data?.favorites || {}; export const selectFavorites = (state: { controller: ControllerState }) => state.controller.data?.favorites ?? EMPTY_OBJECT;
export const selectHistory = (state: { controller: ControllerState }) => state.controller.data?.history || {}; export const selectHistory = (state: { controller: ControllerState }) => state.controller.data?.history ?? EMPTY_OBJECT;
export const selectTopPlayed = (state: { controller: ControllerState }) => state.controller.data?.topPlayed || {}; export const selectTopPlayed = (state: { controller: ControllerState }) => state.controller.data?.topPlayed ?? EMPTY_OBJECT;
export const selectNewSongs = (state: { controller: ControllerState }) => state.controller.data?.newSongs || {}; export const selectNewSongs = (state: { controller: ControllerState }) => state.controller.data?.newSongs ?? EMPTY_OBJECT;
export const selectSongList = (state: { controller: ControllerState }) => state.controller.data?.songList || {}; export const selectSongList = (state: { controller: ControllerState }) => state.controller.data?.songList ?? EMPTY_OBJECT;
export const selectPlayerState = (state: { controller: ControllerState }) => { export const selectPlayerState = (state: { controller: ControllerState }) => {
const playerState = state.controller.data?.player?.state; const playerState = state.controller.data?.player?.state;
@ -173,6 +176,6 @@ export const selectPlayerState = (state: { controller: ControllerState }) => {
return playerState; return playerState;
}; };
export const selectSettings = (state: { controller: ControllerState }) => state.controller.data?.player?.settings; export const selectSettings = (state: { controller: ControllerState }) => state.controller.data?.player?.settings;
export const selectSingers = (state: { controller: ControllerState }) => state.controller.data?.player?.singers || {}; export const selectSingers = (state: { controller: ControllerState }) => state.controller.data?.player?.singers ?? EMPTY_OBJECT;
export default controllerSlice.reducer; export default controllerSlice.reducer;

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../types'; import type { RootState, QueueItem, Singer, Song } from '../types';
import { import {
selectSongs, selectSongs,
selectQueue, selectQueue,
@ -10,7 +10,8 @@ import {
selectSongList, selectSongList,
selectSingers, selectSingers,
selectIsAdmin, selectIsAdmin,
selectCurrentSinger selectCurrentSinger,
selectPlayerState
} from './index'; } from './index';
import { import {
objectToArray, objectToArray,
@ -19,8 +20,7 @@ import {
sortHistoryByDate, sortHistoryByDate,
sortTopPlayedByCount, sortTopPlayedByCount,
sortSongsByArtistAndTitle, sortSongsByArtistAndTitle,
limitArray, limitArray
getQueueStats
} from '../utils/dataProcessing'; } from '../utils/dataProcessing';
import { UI_CONSTANTS } from '../constants'; import { UI_CONSTANTS } from '../constants';
@ -54,7 +54,18 @@ export const selectQueueArray = createSelector(
export const selectQueueStats = createSelector( export const selectQueueStats = createSelector(
[selectQueue], [selectQueue],
(queue) => getQueueStats(queue) (queue) => {
const queueArray = Object.values(queue) as QueueItem[];
const totalSongs = queueArray.length;
const singers = [...new Set(queueArray.map(item => item.singer.name))];
const estimatedDuration = totalSongs * 3; // Rough estimate: 3 minutes per song
return {
totalSongs,
singers,
estimatedDuration,
};
}
); );
export const selectHistoryArray = createSelector( export const selectHistoryArray = createSelector(
@ -92,7 +103,7 @@ export const selectNewSongsArrayWithoutDisabled = createSelector(
export const selectSingersArray = createSelector( export const selectSingersArray = createSelector(
[selectSingers], [selectSingers],
(singers) => objectToArray(singers).sort((a, b) => a.name.localeCompare(b.name)) (singers) => (objectToArray(singers) as Singer[]).sort((a, b) => a.name.localeCompare(b.name))
); );
export const selectSongListArray = createSelector( export const selectSongListArray = createSelector(
@ -104,7 +115,7 @@ export const selectArtistsArray = createSelector(
[selectSongs], [selectSongs],
(songs) => { (songs) => {
const artists = new Set<string>(); const artists = new Set<string>();
Object.values(songs).forEach(song => { (Object.values(songs) as Song[]).forEach((song) => {
if (song.artist) { if (song.artist) {
artists.add(song.artist); artists.add(song.artist);
} }
@ -122,12 +133,12 @@ export const selectTopPlayedArray = createSelector(
export const selectUserQueueItems = createSelector( export const selectUserQueueItems = createSelector(
[selectQueueArray, selectCurrentSinger], [selectQueueArray, selectCurrentSinger],
(queueArray, currentSinger) => (queueArray, currentSinger) =>
queueArray.filter(item => item.singer.name === currentSinger) queueArray.filter((item: QueueItem) => item.singer.name === currentSinger)
); );
export const selectCanReorderQueue = createSelector( export const selectCanReorderQueue = createSelector(
[selectIsAdmin], [selectIsAdmin],
(isAdmin) => isAdmin (isAdmin) => Boolean(isAdmin)
); );
// Search-specific selectors // Search-specific selectors
@ -151,15 +162,40 @@ export const selectSearchResultsWithoutDisabled = createSelector(
// Queue-specific selectors // Queue-specific selectors
export const selectQueueWithUserInfo = createSelector( export const selectQueueWithUserInfo = createSelector(
[selectQueueArray, selectCurrentSinger], [selectQueueArray, selectCurrentSinger],
(queueArray, currentSinger) => (queueArray, currentSinger) => {
queueArray.map(item => ({ // If no items, return empty array
if (queueArray.length === 0) return [];
// If no current singer, return items without isCurrentUser flag
if (!currentSinger) {
return queueArray.map(item => ({
...item,
isCurrentUser: false,
}));
}
// Map items and add isCurrentUser flag
return queueArray.map(item => ({
...item, ...item,
isCurrentUser: item.singer.name === currentSinger, isCurrentUser: item.singer.name === currentSinger,
})) }));
}
); );
// Memoized selector for queue length to prevent unnecessary re-renders // Memoized selector for queue length to prevent unnecessary re-renders
export const selectQueueLength = createSelector( export const selectQueueLength = createSelector(
[selectQueue], [selectQueue],
(queue) => Object.keys(queue).length (queue) => Object.keys(queue).length
);
// Memoized selector for queue object to prevent unnecessary re-renders
export const selectQueueObject = createSelector(
[selectQueue],
(queue) => Object.keys(queue).length > 0 ? { ...queue } : {}
);
// Memoized selector for player state to prevent unnecessary re-renders
export const selectPlayerStateMemoized = createSelector(
[selectPlayerState],
(playerState) => playerState ? { ...playerState } : null
); );

View File

@ -5,6 +5,8 @@ import type { Song, QueueItem, TopPlayed } from '../types';
export const objectToArray = <T extends { key?: string }>( export const objectToArray = <T extends { key?: string }>(
obj: Record<string, T> obj: Record<string, T>
): T[] => { ): T[] => {
if (Object.keys(obj).length === 0) return [];
return Object.entries(obj).map(([key, item]) => ({ return Object.entries(obj).map(([key, item]) => ({
...item, ...item,
key, key,