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 { useNewSongs } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectNewSongs } from '../../redux';
import { selectNewSongsArray } from '../../redux';
import { debugLog } from '../../utils/logger';
import type { Song } from '../../types';
@ -15,8 +15,8 @@ const NewSongs: React.FC = () => {
handleToggleFavorite,
} = useNewSongs();
const newSongs = useAppSelector(selectNewSongs);
const newSongsCount = Object.keys(newSongs).length;
const newSongsArray = useAppSelector(selectNewSongsArray);
const newSongsCount = newSongsArray.length;
// Debug logging
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 { useQueue } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectQueue, selectPlayerState, selectIsAdmin, selectControllerName } from '../../redux';
import { selectQueueLength, selectPlayerStateMemoized, selectIsAdmin, selectControllerName } from '../../redux';
import { PlayerState } from '../../types';
import { queueService } from '../../firebase/services';
import { debugLog } from '../../utils/logger';
@ -23,11 +23,10 @@ const Queue: React.FC = () => {
handleRemoveFromQueue,
} = useQueue();
const queue = useAppSelector(selectQueue);
const playerState = useAppSelector(selectPlayerState);
const queueCount = useAppSelector(selectQueueLength);
const playerState = useAppSelector(selectPlayerStateMemoized);
const isAdmin = useAppSelector(selectIsAdmin);
const controllerName = useAppSelector(selectControllerName);
const queueCount = Object.keys(queue).length;
// Debug logging
debugLog('Queue component - queue count:', queueCount);

View File

@ -1,13 +1,13 @@
import { useCallback } from 'react';
import { useAppSelector } from '../redux';
import { selectControllerName, selectCurrentSinger } from '../redux';
import { selectControllerName, selectCurrentSinger, selectQueueObject } from '../redux';
import { queueService, favoritesService } from '../firebase/services';
import type { Song, QueueItem } from '../types';
export const useSongOperations = () => {
const controllerName = useAppSelector(selectControllerName);
const currentSinger = useAppSelector(selectCurrentSinger);
const currentQueue = useAppSelector((state) => state.controller.data?.player?.queue || {});
const currentQueue = useAppSelector(selectQueueObject);
const addToQueue = useCallback(async (song: Song) => {
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 selectAuthError = (state: { auth: AuthState }) => state.auth.error;
export const selectIsAuthenticated = (state: { auth: AuthState }) => state.auth.data?.authenticated || false;
export const selectCurrentSinger = (state: { auth: AuthState }) => state.auth.data?.singer || '';
export const selectIsAdmin = (state: { auth: AuthState }) => state.auth.data?.isAdmin || false;
export const selectControllerName = (state: { auth: AuthState }) => state.auth.data?.controller || '';
export const selectCurrentSinger = (state: { auth: AuthState }) => state.auth.data?.singer ?? '';
export const selectIsAdmin = (state: { auth: AuthState }) => Boolean(state.auth.data?.isAdmin);
export const selectControllerName = (state: { auth: AuthState }) => state.auth.data?.controller ?? '';
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 selectLastUpdated = (state: { controller: ControllerState }) => state.controller.lastUpdated;
// Constants for empty objects to prevent new references
const EMPTY_OBJECT = {};
// Selectors for specific data
export const selectSongs = (state: { controller: ControllerState }) => state.controller.data?.songs || {};
export const selectQueue = (state: { controller: ControllerState }) => state.controller.data?.player?.queue || {};
export const selectFavorites = (state: { controller: ControllerState }) => state.controller.data?.favorites || {};
export const selectHistory = (state: { controller: ControllerState }) => state.controller.data?.history || {};
export const selectTopPlayed = (state: { controller: ControllerState }) => state.controller.data?.topPlayed || {};
export const selectNewSongs = (state: { controller: ControllerState }) => state.controller.data?.newSongs || {};
export const selectSongList = (state: { controller: ControllerState }) => state.controller.data?.songList || {};
export const selectSongs = (state: { controller: ControllerState }) => state.controller.data?.songs ?? EMPTY_OBJECT;
export const selectQueue = (state: { controller: ControllerState }) => state.controller.data?.player?.queue ?? EMPTY_OBJECT;
export const selectFavorites = (state: { controller: ControllerState }) => state.controller.data?.favorites ?? EMPTY_OBJECT;
export const selectHistory = (state: { controller: ControllerState }) => state.controller.data?.history ?? EMPTY_OBJECT;
export const selectTopPlayed = (state: { controller: ControllerState }) => state.controller.data?.topPlayed ?? EMPTY_OBJECT;
export const selectNewSongs = (state: { controller: ControllerState }) => state.controller.data?.newSongs ?? EMPTY_OBJECT;
export const selectSongList = (state: { controller: ControllerState }) => state.controller.data?.songList ?? EMPTY_OBJECT;
export const selectPlayerState = (state: { controller: ControllerState }) => {
const playerState = state.controller.data?.player?.state;
@ -173,6 +176,6 @@ export const selectPlayerState = (state: { controller: ControllerState }) => {
return playerState;
};
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;

View File

@ -1,5 +1,5 @@
import { createSelector } from '@reduxjs/toolkit';
import type { RootState } from '../types';
import type { RootState, QueueItem, Singer, Song } from '../types';
import {
selectSongs,
selectQueue,
@ -10,7 +10,8 @@ import {
selectSongList,
selectSingers,
selectIsAdmin,
selectCurrentSinger
selectCurrentSinger,
selectPlayerState
} from './index';
import {
objectToArray,
@ -19,8 +20,7 @@ import {
sortHistoryByDate,
sortTopPlayedByCount,
sortSongsByArtistAndTitle,
limitArray,
getQueueStats
limitArray
} from '../utils/dataProcessing';
import { UI_CONSTANTS } from '../constants';
@ -54,7 +54,18 @@ export const selectQueueArray = createSelector(
export const selectQueueStats = createSelector(
[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(
@ -92,7 +103,7 @@ export const selectNewSongsArrayWithoutDisabled = createSelector(
export const selectSingersArray = createSelector(
[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(
@ -104,7 +115,7 @@ export const selectArtistsArray = createSelector(
[selectSongs],
(songs) => {
const artists = new Set<string>();
Object.values(songs).forEach(song => {
(Object.values(songs) as Song[]).forEach((song) => {
if (song.artist) {
artists.add(song.artist);
}
@ -122,12 +133,12 @@ export const selectTopPlayedArray = createSelector(
export const selectUserQueueItems = createSelector(
[selectQueueArray, selectCurrentSinger],
(queueArray, currentSinger) =>
queueArray.filter(item => item.singer.name === currentSinger)
queueArray.filter((item: QueueItem) => item.singer.name === currentSinger)
);
export const selectCanReorderQueue = createSelector(
[selectIsAdmin],
(isAdmin) => isAdmin
(isAdmin) => Boolean(isAdmin)
);
// Search-specific selectors
@ -151,11 +162,24 @@ export const selectSearchResultsWithoutDisabled = createSelector(
// Queue-specific selectors
export const selectQueueWithUserInfo = createSelector(
[selectQueueArray, selectCurrentSinger],
(queueArray, currentSinger) =>
queueArray.map(item => ({
(queueArray, currentSinger) => {
// 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,
isCurrentUser: item.singer.name === currentSinger,
}))
}));
}
);
// Memoized selector for queue length to prevent unnecessary re-renders
@ -163,3 +187,15 @@ export const selectQueueLength = createSelector(
[selectQueue],
(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 }>(
obj: Record<string, T>
): T[] => {
if (Object.keys(obj).length === 0) return [];
return Object.entries(obj).map(([key, item]) => ({
...item,
key,